import {BattleState, EventType, PlayerColor} from './types'
import {Utils} from '../utils'
import {Piece} from './piece'

export class Machine {
  enabled = false
  moving = false
  color: PlayerColor = 'Blue'
  opponentColor: PlayerColor = 'Red'
  level: 'low' | 'middle' | 'high' = 'low'
  private pieces: Piece[][] = []
  private flatten: Piece[] = []
  private timer = 0

  setColor(opponentColor: PlayerColor) {
    this.color = opponentColor === 'Red' ? 'Blue' : 'Red'
    this.opponentColor = opponentColor
    if (this.timer) {
      window.clearTimeout(this.timer)
    }
  }

  setPieces(pieces: Piece[][]) {
    this.pieces = pieces
    this.flatten = Utils.flat(pieces)
  }

  canMove = (color: PlayerColor) => this.enabled && this.color === color

  act(callback, type: EventType, state: BattleState) {
    this.moving = true
    this.timer = window.setTimeout(() => {
      callback(this.pickPiece(type, state))
      this.moving = false
    }, this.delay)
  }

  private pickPiece(type: EventType, state: BattleState) {
    if (state === 'Preparing' && type === 'AiMove') {
      return this.getUnactivated()
    } else if (state === 'Started' && type === 'AiMove') {
      return this.getActivated()
    } else if (type === 'AiMoving') {
      return this.getMovingIn()
    } else if (type === 'AiMarking') {
      return this.getMarking()
    } else if (type === 'AiDeleting') {
      return this.getDeleting()
    }
  }

  getUnactivated() {
    const available = this.flatten.filter(x => x.color === 'Unknown' && x.visible)
    // total 24 pieces, return any piece at beginning
    if (available.length > 22) {
      const obliques = available.filter(x => x.obliqueGroup)
      return obliques [Utils.getRandomInt(obliques.length)]
    } else {
      // * * o return o
      for (let p of available) {
        if (p.canSay3(this.pieces, this.color, 0)) {
          return p
        }
      }

      // x x o return the o to stop opponent
      for (let p of available) {
        if (p.canSay3(this.pieces, this.opponentColor, 0)) {
          return p
        }
      }

      // find which has two change say3
      // * o o return the first o
      // o * o
      for (let p of available) {
        if (Piece.getSay3Pieces(p, this.pieces)
          .filter(row => Piece.hasActivatedColors(row, this.color, 1) && Piece.hasColors(row, 'Unknown', 2)).length > 1) {
          return p
        }
      }

      // return piece which next step can say3, and next next can say3 too, also can stop opponent
      for (let p of available) {
        const say3pieces = Piece.getSay3Pieces(p, this.pieces)
        if (say3pieces.some(row => Piece.hasActivatedColors(row, this.opponentColor, 1) && Piece.hasColors(row, 'Unknown', 2)) &&
          say3pieces.some(row => Piece.hasActivatedColors(row, this.color, 1) && Piece.hasColors(row, 'Unknown', 2)) &&
          say3pieces.some(row => Piece.hasColors(row, 'Unknown', 3))) {
          return p
        }
      }

      // return piece which next step can say3, and next next can say3 too
      for (let p of available) {
        const say3pieces = Piece.getSay3Pieces(p, this.pieces)
        if (say3pieces.length == 3 && say3pieces.some(row => Piece.hasActivatedColors(row, this.opponentColor, 1)) &&
          say3pieces.some(row => Piece.hasActivatedColors(row, this.color, 1) && Piece.hasColors(row, 'Unknown', 2)) &&
          say3pieces.some(row => Piece.hasColors(row, 'Unknown', 3))) {
          return p
        }
      }

      // return piece which next step can say3
      for (let p of available) {
        const say3pieces = Piece.getSay3Pieces(p, this.pieces)
        if (say3pieces.length == 3 && say3pieces.some(row => Piece.hasActivatedColors(row, this.color, 1) && Piece.hasColors(row, 'Unknown', 2))) {
          return p
        }
      }

      // stop opponent which has two chance to say3
      for (let p of available) {
        if (Piece.getSay3Pieces(p, this.pieces)
          .filter(row => Piece.hasActivatedColors(row, this.opponentColor, 1) &&
            Piece.hasColors(row, 'Unknown', 2)).length > 1) {
          return p
        }
      }

      // * * 0
      // o o o // return last o
      if (available.length < 5) {
        for (let p of available) {
          const neighbors = Piece.getNeighbors(p, this.pieces, false)
          for (let np of neighbors) {
            if (np.state === 'Marked' && Piece.getSay3Pieces(np, this.pieces).some(row => Piece.hasActivatedColors(row, this.color, 2))) {
              return p
            }
          }
        }

      }

      // return unactivated piece from neighbors
      for (let p of available) {
        const say3pieces = Piece.getSay3Pieces(p, this.pieces)
        if (say3pieces.some(row => row.some(x => x.color === this.color))) {
          return p
        }
      }

      return available[Utils.getRandomInt(available.length - 1)]
    }
  }

  getActivated() {
    const allActivated = this.flatten.filter(x =>
      x.visible &&
      x.state === 'Activated' &&
      x.color === this.color && Piece.getNeighbors(x, this.pieces, true).length > 0)

    // find loop
    const loopKey = Machine.findLoopKey(this.pieces, this.color)
    if (loopKey) {
      return loopKey
    }

    // * * o
    // * * o
    // o o * make loop say3
    for (let p of allActivated) {
      let clone = Utils.clone(this.pieces)
      const p1 = clone[p.rowIndex][p.colIndex]
      p1.color = 'Unknown'
      p1.state = 'Deleted'
      for (let np of Piece.getNeighbors(p1, clone, true)) {
        np.state = 'Activated'
        np.color = this.color
        if (Piece.getSay3Pieces(np, clone).some(row => row.every(x => x.color === this.color)) &&
          Machine.findLoopKey(clone, this.color)) {
          return p
        }
      }
    }
    // * * x
    // * * o
    // o o * make loop say3 with opponent
    for (let p of allActivated) {
      let clone = Utils.clone(this.pieces)
      const p1 = clone[p.rowIndex][p.colIndex]
      p1.color = 'Unknown'
      p1.state = 'Deleted'
      for (let np of Piece.getNeighbors(p1, clone, true)) {
        np.state = 'Activated'
        np.color = this.color
        if (Piece.getSay3Pieces(np, clone).some(row => row.every(x => x.color === this.color)) &&
          Machine.getLoopSay3(clone, this.color, this.opponentColor)) {
          return p
        }
      }
    }

    // one step to say3
    for (let p of allActivated) {
      if (p.canSay3(this.pieces, this.color, 1)) {
        return p
      }
    }
    // x x o
    // * * *
    // two steps to say3 and stop opponent say3
    for (let p of allActivated) {
      const say3Pieces = Piece.getSay3Pieces(p, this.pieces)
      for (let row of say3Pieces) {
        if (Piece.hasColors(row, this.color, 3)) {
          for (let p1 of row) {
            if (Piece.getNeighbors(p1, this.pieces, true).some(x => {
              return Piece.getSay3Pieces(x, this.pieces).some(row1 => Piece.hasColors(row1, this.opponentColor, 2)) &&
                Piece.getNeighbors(x, this.pieces, false).filter(np => np.color === this.opponentColor).length >= 2
            })) {
              return p1
            }
          }
        }
      }
    }

    // stop opponent say3
    for (let p of allActivated) {
      if (Piece.getNeighbors(p, this.pieces, true).some(x => {
        const say3pieces = Piece.getSay3Pieces(x, this.pieces)
        const say3row = say3pieces.find(row => Piece.hasColors(row, this.opponentColor, 2) && Piece.hasColors(row, 'Unknown', 1))
        return say3row && Piece.getNeighbors(say3row.find(x => x.color === 'Unknown'), this.pieces, false)
          .filter(x => x.color === this.opponentColor).length >= 2
      })) {
        return p
      }
    }

    for (let p of allActivated) {
      const say3Rows = Piece.getSay3Pieces(p, this.pieces)
      for (let row of say3Rows) {
        if (Piece.hasActivatedColors(row, this.color, 3)) {
          for (let p1 of row) {
            const neighbours = Piece.getNeighbors(p1, this.pieces, false)
            if (neighbours.every(x => x.color !== this.opponentColor) && neighbours.some(x => x.color === 'Unknown')) {
              return p1
            }
          }
        }
      }
    }

    for (let p of allActivated) {
      if (p.canSay3(this.pieces, this.color, 2)) {
        return p
      }
    }

    // todo find which on can say3 in 3 steps

    return allActivated[Utils.getRandomInt(allActivated.length - 1)]
  }

  getMarking() {
    const allMarking = this.flatten.filter(x => x.visible && x.state === 'Marking')
    // mark piece which has two chances say3
    for (let p of allMarking) {
      if (Piece.getSay3Pieces(p, this.pieces).filter(row => {
        return row.filter(x => x.color === this.opponentColor && x.state === 'Marking').length > 1 && Piece.hasColors(row, 'Unknown', 1)
      }).length > 1) {
        return p
      }
    }


    const opponentLoops = Machine.getLoopSay3(this.pieces, this.opponentColor, this.color)
    if (opponentLoops && opponentLoops.every(row => row.filter(x => x.color === this.opponentColor && x.state === 'Marking').length >= 2)) {
      for (let i = 0; i < opponentLoops.length; i++) {
        const myPiece = opponentLoops[i].find(x => x.color !== this.opponentColor)
        if (myPiece && myPiece.state === 'Marked') {
          return Piece.getNeighbors(myPiece, this.pieces, false).find(n => opponentLoops[1 - i].includes(n))
        }
      }
    }

    // mark piece which has one chances say3
    for (let p of allMarking) {
      if (Piece.getSay3Pieces(p, this.pieces).some(row => {
        return row.filter(x => x.color === this.opponentColor && x.state === 'Marking').length > 1 && Piece.hasColors(row, 'Unknown', 1)
      })) {
        return p
      }
    }

    // * * x // mark this x
    // * * *
    const myLoops = Machine.getLoopSay3(this.pieces, this.color, this.opponentColor)
    if (myLoops && myLoops.every(row => row.filter(x => x.color === this.opponentColor && x.state === 'Activated').length >= 2)) {
      for (let i = 0; i < myLoops.length; i++) {
        const opponentPiece = myLoops[i].find(x => x.color !== this.color)
        if (opponentPiece && opponentPiece.state !== 'Marked') {
          return Piece.getNeighbors(opponentPiece, this.pieces, false).find(n => myLoops[1 - i].includes(n))
        }
      }
    }

    const loopOpponentColor = Machine.findLoopOpponentColor(this.pieces, this.color, this.opponentColor)
    if (loopOpponentColor && loopOpponentColor) {
      return loopOpponentColor
    }

    // mark piece which close to say3
    for (let p of allMarking) {
      if (Piece.getSay3Pieces(p, this.pieces).some(row => {
        return row.some(x => x.color === this.opponentColor && x.state === 'Marking') && Piece.hasColors(row, 'Unknown', 2)
      })) {
        return p
      }
    }

    return allMarking[Utils.getRandomInt(allMarking.length - 1)]
  }

  getMovingIn() {
    const allMoving = this.flatten.filter(x => x.visible && x.state === 'MovingIn')
    for (let p of allMoving) {
      if (p.canSay3(this.pieces, this.color, 0)) {
        return p
      }
    }

    const opponentLoops = Machine.getLoopSay3(this.pieces, this.opponentColor, 'Unknown')
    if (opponentLoops) {
      for (let row of opponentLoops) {
        const unknown = row.find(x => x.color === 'Unknown')
        if (allMoving.includes(unknown)) {
          return unknown
        }
      }
    }

    for (let p of allMoving) {
      if (p.canSay3(this.pieces, this.opponentColor, 0)) {
        return p
      }
    }
    for (let p of allMoving) {
      if (p.canSay3(this.pieces, this.color, 1)) {
        return p
      }
    }
    return allMoving[Utils.getRandomInt(allMoving.length - 1)]
  }

  getDeleting(): Piece {
    const allDeleting = this.flatten.filter(x => x.visible && x.state === 'Deleting')

    const opponentLoopKey = Machine.findLoopKey(this.pieces, this.opponentColor)
    if (opponentLoopKey) {
      return opponentLoopKey
    }

    // x o x
    // o x o delete this x
    // x o x
    for (let p of allDeleting) {
      const neighbors = Piece.getNeighbors(p, this.pieces, true)
      if (neighbors.filter(np => {
        const say3Rows = Piece.getSay3Pieces(np, this.pieces)
        return say3Rows.some(row => Piece.hasColors(row, this.opponentColor, 2))
      }).length > 1) {
        return p
      }
    }

    // o x x
    // x x o delete this x
    // o o x
    for (let p of allDeleting) {
      const say3Pieces = Piece.getSay3Pieces(p, this.pieces)
        .find(x => Piece.hasColors(x, this.opponentColor, 2) && Piece.hasColors(x, 'Unknown', 1))
      if (p.canSay3(this.pieces, this.opponentColor, 1) && say3Pieces && Piece.getNeighbors(say3Pieces.find(x => x.color === 'Unknown'), this.pieces, false)
        .some(x => x.color === this.opponentColor)) {
        return p
      }
    }
    // x x o delete this x
    // o o x
    for (let p of allDeleting) {
      const say3Pieces = Piece.getSay3Pieces(p, this.pieces)
        .find(x => Piece.hasColors(x, this.opponentColor, 2) && Piece.hasColors(x, 'Unknown', 1))
      if (say3Pieces && Piece.getNeighbors(say3Pieces.find(x => x.color === 'Unknown'), this.pieces, false)
        .some(x => x.color === this.opponentColor && !say3Pieces.includes(x))) {
        return p
      }
    }

    // x x o
    // o o x delete this x
    for (let p of allDeleting) {
      if (p.canSay3(this.pieces, this.opponentColor, 1)) {
        return p
      }
    }

    // o o x // delete this x
    // * * o
    // * * *
    const loopUnknown = Machine.findLoopOpponentColor(this.pieces, this.color, 'Unknown')
    if (loopUnknown) {
      for (let p of Piece.getNeighbors(loopUnknown, this.pieces, false)) {
        if (p.color === this.opponentColor) {
          return p
        }
      }
    }

    // * * x // delete this x
    // * * *
    const loopOpponentColor = Machine.findLoopOpponentColor(this.pieces, this.color, this.opponentColor)
    if (loopOpponentColor) {
      return loopOpponentColor
    }

    // * * x // delete this x
    // o o *
    for (let p of allDeleting) {
      const say3rows = Piece.getSay3Pieces(p, this.pieces)
      if (say3rows.some(row => Piece.hasColors(row, this.color, 2))) {
        for (let np of Piece.getNeighbors(p, this.pieces, false)) {
          if (np.color === this.color && say3rows.every(row => !row.includes(np))) {
            return p
          }
        }
      }
    }

    // delete piece which already say3
    for (let p of allDeleting) {
      const say3Pieces = Piece.getSay3Pieces(p, this.pieces)
      for (let row of say3Pieces) {
        if (Piece.hasColors(row, this.opponentColor, 3)) {
          for (let p1 of row) {
            if (Piece.getNeighbors(p1, this.pieces, false).filter(x => !row.includes(x)).every(x => x.color !== this.opponentColor)) {
              return p1
            }
          }
        }
      }
    }

    return allDeleting[Utils.getRandomInt(allDeleting.length - 1)]
  }

  static getLoopSay3(pieces: Piece[][], color: PlayerColor, missColor: PlayerColor) {
    for (let boardRow of pieces) {
      for (let p of boardRow.filter(x => x.visible)) {
        const say3pieces = Piece.getSay3Pieces(p, pieces)
        for (let row of say3pieces) {
          // find * * *
          if (Piece.hasColors(row, color, 3)) {
            for (let p1 of row) {
              for (let np of Piece.getNeighbors(p1, pieces, false).filter(x => x.color !== color)) {
                const say3neighbors = Piece.getSay3Pieces(np, pieces).filter(row1 => !row.some(x => row1.includes(x)))
                const found = say3neighbors.find(x => Piece.hasColors(x, color, 2) && Piece.hasColors(x, missColor, 1))

                if (found) {
                  return [row, found]
                }
              }
            }
          }
        }
      }
    }
  }

  // * * x
  // * * * // return this x
  static findLoopKey(pieces: Piece[][], color: PlayerColor): Piece {
    const loops = Machine.getLoopSay3(pieces, color, 'Unknown')
    if (loops) {
      for (let i = 0; i < loops.length; i++) {
        const unknown = loops[i].find(x => x.color !== color)
        if (unknown) {
          return Piece.getNeighbors(unknown, pieces, false).find(n => loops[1 - i].includes(n))
        }
      }
    }
  }

  // * * x // return this x
  // * * *
  static findLoopOpponentColor(pieces: Piece[][], color: PlayerColor, missingColor: PlayerColor): Piece {
    const loops = Machine.getLoopSay3(pieces, color, missingColor)
    if (loops) {
      const opponentPiece = Utils.flat(loops).find(x => x.color === missingColor)
      if (opponentPiece) {
        return opponentPiece
      }
    }
  }

  private get delay() {
    return Utils.getRandomInt(2000, 500)
    // switch (this.level) {
    //   case "high":
    //     return Utils.getRandomInt(2000, 500)
    //   case "middle":
    //     return Utils.getRandomInt(3000, 1000)
    //   case "low":
    //     return Utils.getRandomInt(4000, 2000)
    // }
  }
}